﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Dokan;
using bkfviewer;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace BKF4Dokan {
    class Program {
        TraceSource appTrace = new TraceSource("BKF4Dokan", SourceLevels.Information);

        static void Main(string[] args) {
            if (args.Length < 1) {
                Console.Error.WriteLine("BKF4Dokan your.bkf N");
                Environment.Exit(1);
            }
            new Program().Run(args[0], args.Length >= 2 ? args[1][0] : '?');
        }

        public Program() {
            appTrace.Listeners.Add(new ConsoleTraceListener(true));
        }

        const int MinBlkSize = 1024;

        int alignBytes = MinBlkSize;

        List<Int64> aloffSfmb = new List<Int64>();

        void AddItem(String name, Int64 off, int indent, object header) { }

        BKFClient bkftech = new BKFClient();

        class Timing {
            DateTime dt = DateTime.Now;

            public bool Tick() {
                if (dt <= DateTime.Now) {
                    dt = DateTime.Now.AddSeconds(10);
                    return true;
                }
                return false;
            }
        }

        Timing Ticker = new Timing();

        void Walk(Stream si) {
            String lastSet = null, lastVol = null, lastDir = null, lastFile = null;
            while (si.Position < si.Length) {
                Int64 off = si.Position;
                DateTime? dtMt = null, dtCt = null, dtAt = null;

                bool hasStream = true;

                if (Ticker.Tick()) {
                    appTrace.TraceEvent(TraceEventType.Information, 200, "# Wait a moment. " + bkftech.EntryCount.ToString("#,##0") + " files identified @ " + DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss") + ".");
                }

                si.Flush();
                si.Position = off;
                byte[] binHdr = BSt.ReadBytes(si, MinBlkSize);
                FileAttributes atts = FileAttributes.Normal;
                MTF_DB_HDR header = new MTF_DB_HDR(binHdr);
                if (header.DBLKName == "TAPE") {
                    MTF_TAPE1 tapeHeader = new MTF_TAPE1(binHdr);
                    AddItem(header.DBLKName, off, 0, tapeHeader);
                    alignBytes = tapeHeader.FormatLogicalBlockSize;

                    if (0 == (si.Length & (alignBytes - 1))) {
                        si.Flush();
                        si.Position = si.Length - alignBytes;

                        byte[] sfmbHdr = BSt.ReadBytes(si, MinBlkSize);
                        MTF_SFMB1 myHdr = new MTF_SFMB1(sfmbHdr);

                        if (myHdr.DBLKName == "SFMB") {
                            foreach (Int64 newoff in myHdr.PBAofPreviousFilemarksArray) {
                                aloffSfmb.Add(newoff * alignBytes);
                            }

                            aloffSfmb.Sort();
                        }
                    }
                }
                else if (header.DBLKName == "SFMB") {
                    MTF_SFMB1 myHdr = new MTF_SFMB1(binHdr);
                    AddItem(header.DBLKName, off, 0, myHdr);
                    hasStream = false;
                    while (aloffSfmb.Remove(off)) { }
                }
                else if (header.DBLKName == "SSET") {
                    MTF_SSET1 myHdr = new MTF_SSET1(binHdr);
                    AddItem(header.DBLKName, off, 0, myHdr);

                    lastSet = myHdr.DataSetNameText;
                    lastVol = lastDir = lastFile = null;
                }
                else if (header.DBLKName == "VOLB") {
                    MTF_VOLB1 myHdr = new MTF_VOLB1(binHdr);
                    AddItem(header.DBLKName, off, 0, myHdr);

                    lastVol = myHdr.DeviceNameText;
                    lastDir = lastFile = null;
                }
                else if (header.DBLKName == "DIRB") {
                    MTF_DIRB1 myHdr = new MTF_DIRB1(binHdr);
                    AddItem(header.DBLKName, off, 0, myHdr);

                    lastDir = myHdr.DirectoryNameText;
                    lastFile = null;

                    String fp = FPUt.CleanName(lastSet).Trim('\\')
                        + "\\" + FPUt.CleanVol(lastVol.Replace('/', '\\')).Trim('\\')
                        + "\\" + FPUt.CleanDir(lastDir.Replace('/', '\\')).Trim('\\')
                        ;
                    bkftech.AddEntry(fp, new FEntry(-2, -2, FileAttributes.Directory | (myHdr.DIRB_HIDDEN_BIT ? FileAttributes.Hidden : 0) | (myHdr.DIRB_SYSTEM_BIT ? FileAttributes.System : 0) | (myHdr.DIRB_READ_ONLY_BIT ? FileAttributes.ReadOnly : 0), dtMt, dtCt, dtAt));
                }
                else if (header.DBLKName == "FILE") {
                    MTF_FILE1 myHdr = new MTF_FILE1(binHdr);
                    AddItem(header.DBLKName, off, 0, myHdr);

                    lastFile = myHdr.FileNameText;
                    dtMt = CUt.Get(myHdr.LastModificationDate);
                    dtCt = CUt.Get(myHdr.CreationDate);
                    dtAt = CUt.Get(myHdr.LastAccessDate);

                    atts = FileAttributes.Normal
                        | (myHdr.FILE_SYSTEM_BIT ? FileAttributes.System : 0)
                        | (myHdr.FILE_HIDDEN_BIT ? FileAttributes.Hidden : 0)
                        | (myHdr.FILE_READ_ONLY_BIT ? FileAttributes.ReadOnly : 0);
                }
                else if (header.DBLKName == "EOTM") {
                    MTF_EOTM1 myHdr = new MTF_EOTM1(binHdr);
                    AddItem(header.DBLKName, off, 0, myHdr);
                }
                else if (header.DBLKName == "ESET") {
                    MTF_ESET1 myHdr = new MTF_ESET1(binHdr);
                    AddItem(header.DBLKName, off, 0, myHdr);
                }
                else {
                    AddItem(header.DBLKName, off, 0, header);
                }

                si.Position = off = (off + header.OffsetToFirstEvent);

                if (hasStream) {
                    bool hasStan = false;
                    while (true) {
                        si.Position = off = RUt.RoundUpTo4(off);

                        MTF_STREAM_HDR streamHeader = new MTF_STREAM_HDR(BSt.ReadBytes(si, 22));

                        AddItem(streamHeader.StreamID4CC, off, 1, streamHeader);

                        if (lastSet != null && lastVol != null && lastDir != null && lastFile != null) {
                            if (streamHeader.StreamID4CC == "STAN") {
                                hasStan = true;
                                String fp = FPUt.CleanName(lastSet).Trim('\\')
                                    + "\\" + FPUt.CleanVol(lastVol.Replace('/', '\\')).Trim('\\')
                                    + "\\" + FPUt.CleanDir(lastDir.Replace('/', '\\')).Trim('\\')
                                    + "\\" + FPUt.CleanName(lastFile.Replace('/', '\\')).Trim('\\')
                                    ;
                                bkftech.AddEntry(fp, new FEntry(si.Position, Convert.ToInt64(streamHeader.StreamLength), atts, dtMt, dtCt, dtAt));
                            }
                        }

                        off = si.Position + Convert.ToInt64(streamHeader.StreamLength);

                        if (streamHeader.StreamID4CC == "SPAD") {
                            break;
                        }
                    }

                    if (!hasStan
                        && !String.IsNullOrEmpty(lastSet)
                        && !String.IsNullOrEmpty(lastVol)
                        && !String.IsNullOrEmpty(lastDir)
                        && !String.IsNullOrEmpty(lastFile)
                    ) {
                        String fp = FPUt.CleanName(lastSet).Trim('\\')
                            + "\\" + FPUt.CleanVol(lastVol.Replace('/', '\\')).Trim('\\')
                            + "\\" + FPUt.CleanDir(lastDir.Replace('/', '\\')).Trim('\\')
                            + "\\" + FPUt.CleanName(lastFile.Replace('/', '\\')).Trim('\\')
                            ;
                        bkftech.AddEntry(fp, new FEntry(si.Position, Convert.ToInt64(0), atts, dtMt, dtCt, dtAt));
                    }

                    lastFile = null;

                    si.Position = RUt.RoundUpTo4(off);
                }
            }
        }

        class FPUt {
            public static string CleanName(String fn) {
                return Regex.Replace(fn, "[\\\\\\/\\*\\?\\<\\>\\|\\\"\\:]", "_");
            }

            public static string CleanVol(String fn) {
                if (fn.Length == 2 && char.IsLetter(fn[0]) && fn[1] == ':') {
                    return fn.Substring(0, 1);
                }
                return Regex.Replace(fn, "[\\\\\\/\\*\\?\\<\\>\\|\\\"\\:]", "_");
            }

            public static string CleanDir(String fn) {
                return Regex.Replace(fn, "[\\*\\?\\<\\>\\|\\\"\\:]", "_");
            }
        }

        class BSt {
            public static byte[] ReadBytes(Stream si, int cb) {
                byte[] bin = new byte[cb];
                for (int x = 0; x < cb; ) {
                    int r = si.Read(bin, x, cb - x);
                    if (r < 1) throw new EndOfStreamException();
                    x += r;
                }
                return bin;
            }
        }

        class RUt {
            public static Int64 RoundUpTo4(Int64 val) {
                return (val + 3) & (~3);
            }
        }

        class CUt {
            public static DateTime? Get(MTF_DATE_TIME dt) {
                return (dt.HasValue) ? dt.Value : new DateTime?();
            }
        }

        void Run(String fpbkf, char letter) {
            using (FileStream fs = File.Open(fpbkf, FileMode.Open, FileAccess.Read, FileShare.Read)) {
                bkftech.BKFStream = fs;

                {
                    appTrace.TraceEvent(TraceEventType.Information, 200, "# Parsing bkf structure.");
                    try {
                        Walk(fs);
                    }
                    catch (Exception err) {
                        appTrace.TraceEvent(TraceEventType.Warning, 500, "& Parsing error: " + err);
                    }
                    appTrace.TraceEvent(TraceEventType.Information, 200, "# Parsing has been done. " + bkftech.EntryCount.ToString("#,##0") + " files.");
                }

                {
                    if (letter == '?') {
                        String[] drvs = Environment.GetLogicalDrives();
                        for (int x = 2; x < 26; x++) {
                            char c = (char)('A' + x);
                            bool found = false;
                            foreach (String drv in drvs) {
                                if (drv[0] == c) {
                                    found = true;
                                    break;
                                }
                            }
                            if (!found) {
                                letter = c;
                                break;
                            }
                        }
                    }

                    appTrace.TraceEvent(TraceEventType.Information, 200, "# Dokan runs. Path: {0}:\\", letter);

                    DokanOptions opt = new DokanOptions();
                    //opt.DebugMode = true;
                    opt.MountPoint = letter + ":\\";
                    opt.ThreadCount = 3;
                    DokanNet.DokanMain(opt, bkftech);

                    appTrace.TraceEvent(TraceEventType.Information, 200, "# Dokan exits.");
                }
            }
        }
    }

    class FEntry {
        public Int64 off, size;
        public DateTime? dtMt, dtCt, dtAt;
        public uint fno;
        public FileAttributes atts;

        public bool IsDir { get { return 0 != (atts & FileAttributes.Directory); } }

        public FEntry(Int64 off, Int64 size, FileAttributes atts, DateTime? dtMt, DateTime? dtCt, DateTime? dtAt) {
            this.off = off;
            this.size = size;
            this.atts = atts;
            this.dtMt = dtMt;
            this.dtCt = dtCt;
            this.dtAt = dtAt;
        }
    }

    class DLeaf {
        public SortedDictionary<String, DLeaf> dictDir = new SortedDictionary<String, DLeaf>(StringComparer.InvariantCultureIgnoreCase);
        public SortedDictionary<String, FEntry> dictFile = new SortedDictionary<string, FEntry>();

        public FEntry fe;

        public DLeaf(FEntry fe) {
            this.fe = fe;
        }
    }

    class BKFClient : DokanOperations {
        DLeaf root = new DLeaf(new FEntry(-3, -3, FileAttributes.Directory, null, null, null));

        UInt32 fno = 1;
        Int64 sizeArc = 0;
        int cntFiles = 0;
        Stream fs = null;

        TraceSource appTrace = new TraceSource("BKFClient", SourceLevels.Warning);

        public BKFClient() {
            appTrace.Listeners.Add(new ConsoleTraceListener(true));
        }

        public Stream BKFStream { get { return fs; } set { fs = value; } }

        bool FindEntry(string filename, out FEntry fe) {
            String[] cols = filename.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
            DLeaf leaf = root;
            for (int x = 0; x < cols.Length; x++) {
                DLeaf via;
                if (leaf.dictDir.TryGetValue(cols[x], out via)) {
                    leaf = via;
                }
                else if (leaf.dictFile.TryGetValue(cols[x], out fe)) {
                    return true;
                }
                else {
                    fe = null;
                    return false;
                }
            }
            fe = leaf.fe;
            return true;
        }

        DLeaf FindLeaf(string filename) {
            String[] cols = filename.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
            DLeaf leaf = root;
            for (int x = 0; x < cols.Length; x++) {
                DLeaf via;
                if (leaf.dictDir.TryGetValue(cols[x], out via)) {
                    leaf = via;
                }
                else {
                    return null;
                }
            }
            return leaf;
        }

        public void AddEntry(String fp, FEntry o) {
            o.fno = fno++;
            sizeArc += (o.size > 0) ? o.size : 0;

            String[] cols = fp.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries);
            DLeaf leaf = root;
            for (int x = 0; x < cols.Length; x++) {
                bool last = (x + 1 == cols.Length);
                if (!last) {
                    DLeaf via;
                    if (leaf.dictDir.TryGetValue(cols[x], out via)) {
                        leaf = via;
                    }
                    else {
                        leaf = leaf.dictDir[cols[x]] = new DLeaf(new FEntry(-2, -2, FileAttributes.Directory, null, null, null));
                    }
                }
                else if (o.IsDir) {
                    leaf.dictDir[cols[x]] = new DLeaf(o);
                }
                else {
                    leaf.dictFile[cols[x]] = o;
                    cntFiles++;
                }
            }
        }

        public int EntryCount { get { return cntFiles; } }

        class Openf {
            FEntry fe;

            public FEntry Entry { get { return fe; } }

            public Openf(FEntry fe) {
                this.fe = fe;
            }
        }

        Object FileSync = new Object();

        #region DokanOperations メンバ

        public int CreateFile(string filename, FileAccess access, FileShare share, FileMode mode, FileOptions options, DokanFileInfo info) {
            if (filename.StartsWith("\\")) filename = filename.Substring(1);
            FEntry fe;
            if (FindEntry(filename, out fe)) {
                if (fe.IsDir) {

                }
                else {
                    if (access != FileAccess.Read) return -DokanNet.ERROR_ACCESS_DENIED;
                    if (mode != FileMode.Open && mode != FileMode.OpenOrCreate) return -DokanNet.ERROR_ACCESS_DENIED;
                }
                info.Context = new Openf(fe);
                info.IsDirectory = fe.IsDir;
                info.InfoId = fe.fno;
                appTrace.TraceEvent(TraceEventType.Information, 200, "# Openf: " + filename);
                return 0;
            }

            if (mode != FileMode.Open) return -DokanNet.ERROR_ACCESS_DENIED;
            return -DokanNet.ERROR_FILE_NOT_FOUND;
        }

        public int OpenDirectory(string filename, DokanFileInfo info) {
            if (filename.StartsWith("\\")) filename = filename.Substring(1);
            FEntry fe;
            if (FindEntry(filename, out fe)) {
                if (fe.IsDir) {
                    info.Context = new Openf(fe);
                    info.IsDirectory = true;
                    info.InfoId = fe.fno;
                    appTrace.TraceEvent(TraceEventType.Information, 200, "# Opend: " + filename);
                    return 0;
                }
                return -DokanNet.ERROR_ACCESS_DENIED;
            }
            return -DokanNet.ERROR_PATH_NOT_FOUND;
        }

        public int CreateDirectory(string filename, DokanFileInfo info) {
            return -DokanNet.ERROR_ACCESS_DENIED;
        }

        public int Cleanup(string filename, DokanFileInfo info) {
            return 0;
        }

        public int CloseFile(string filename, DokanFileInfo info) {
            if (filename.StartsWith("\\")) filename = filename.Substring(1);
            if (info.Context != null) {
                info.Context = null;
                appTrace.TraceEvent(TraceEventType.Information, 200, "# Close: " + filename);
                return 0;
            }
            return -DokanNet.ERROR_INVALID_NAME;
        }

        public int ReadFile(string filename, byte[] buffer, ref uint readBytes, long offset, DokanFileInfo info) {
            Openf f = info.Context as Openf;
            if (f != null) {
                lock (FileSync) {
                    int size = Math.Min(buffer.Length, Math.Max(0, Convert.ToInt32(f.Entry.size - offset)));
                    fs.Position = f.Entry.off + offset;
                    readBytes = Convert.ToUInt32(fs.Read(buffer, 0, size));
                    return 0;
                }
            }
            return -DokanNet.ERROR_FILE_NOT_FOUND;
        }

        public int WriteFile(string filename, byte[] buffer, ref uint writtenBytes, long offset, DokanFileInfo info) {
            return -DokanNet.ERROR_ACCESS_DENIED;
        }

        public int FlushFileBuffers(string filename, DokanFileInfo info) {
            return 0;
        }

        DateTime EPOC { get { return new DateTime(1900, 1, 1); } }

        FileInformation Fill(FileInformation fileinfo, FEntry fe) {
            fileinfo.Attributes = fe.atts;
            fileinfo.CreationTime = fe.dtCt ?? EPOC;
            fileinfo.LastAccessTime = fe.dtAt ?? EPOC;
            fileinfo.LastWriteTime = fe.dtMt ?? EPOC;
            fileinfo.Length = fe.IsDir ? 0 : fe.size;
            return fileinfo;
        }

        public int GetFileInformation(string filename, FileInformation fileinfo, DokanFileInfo info) {
            if (filename.StartsWith("\\")) filename = filename.Substring(1);
            FEntry fe;
            if (FindEntry(filename, out fe)) {
                Fill(fileinfo, fe);
                fileinfo.FileName = filename;
                info.IsDirectory = fe.IsDir;
                return 0;
            }
            return -DokanNet.ERROR_PATH_NOT_FOUND;
        }

        public int FindFiles(string filename, System.Collections.ArrayList files, DokanFileInfo info) {
            if (filename.StartsWith("\\")) filename = filename.Substring(1);
            SortedDictionary<string, object> dictRead = new SortedDictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
            DLeaf leaf = FindLeaf(filename);
            if (leaf != null) {
                foreach (KeyValuePair<String, DLeaf> kv in leaf.dictDir) {
                    FileInformation fileinfo = Fill(new FileInformation(), kv.Value.fe);
                    fileinfo.FileName = kv.Key;
                    files.Add(fileinfo);
                }
                foreach (KeyValuePair<String, FEntry> kv in leaf.dictFile) {
                    FileInformation fileinfo = Fill(new FileInformation(), kv.Value);
                    fileinfo.FileName = kv.Key;
                    files.Add(fileinfo);
                }
                return 0;
            }
            return -DokanNet.ERROR_PATH_NOT_FOUND;
        }

        public int SetFileAttributes(string filename, FileAttributes attr, DokanFileInfo info) {
            return -DokanNet.ERROR_ACCESS_DENIED;
        }

        public int SetFileTime(string filename, DateTime ctime, DateTime atime, DateTime mtime, DokanFileInfo info) {
            return -DokanNet.ERROR_ACCESS_DENIED;
        }

        public int DeleteFile(string filename, DokanFileInfo info) {
            return -DokanNet.ERROR_ACCESS_DENIED;
        }

        public int DeleteDirectory(string filename, DokanFileInfo info) {
            return -DokanNet.ERROR_ACCESS_DENIED;
        }

        public int MoveFile(string filename, string newname, bool replace, DokanFileInfo info) {
            return -DokanNet.ERROR_ACCESS_DENIED;
        }

        public int SetEndOfFile(string filename, long length, DokanFileInfo info) {
            return -DokanNet.ERROR_ACCESS_DENIED;
        }

        public int SetAllocationSize(string filename, long length, DokanFileInfo info) {
            return -DokanNet.ERROR_ACCESS_DENIED;
        }

        public int LockFile(string filename, long offset, long length, DokanFileInfo info) {
            return 0;
        }

        public int UnlockFile(string filename, long offset, long length, DokanFileInfo info) {
            return 0;
        }

        public int GetDiskFreeSpace(ref ulong freeBytesAvailable, ref ulong totalBytes, ref ulong totalFreeBytes, DokanFileInfo info) {
            totalBytes = Convert.ToUInt64(sizeArc);
            freeBytesAvailable = totalFreeBytes = 0;
            return 0;
        }

        public int Unmount(DokanFileInfo info) {
            return 0;
        }

        #endregion
    }
}
